import { createClient } from "redis";
import { isEmpty, timeConvert, TimeUnit, TimeUnitType } from "st-common-core";

/**
 * 操作 Redis 的客户端
 */
let client;

/**
 * 连接 Redis，创建并保存 Redis 连接对象
 *
 * @param {string} username 用户名
 * @param {string} password 密码
 * @param {string} host 主机
 * @param {number} port 端口
 * @param {number} db 数据库
 * @param {boolean} isLog 是否打印日志
 * @param {function} errHandler 错误事件处理函数
 * @param {function} connectHandler 连接成功事件处理函数
 * @param {function} readyHandler 准备就绪事件处理函数
 * @param {function} reconnectingHandler 重连事件处理函数
 * @param {function} endHandler 断开连接事件处理函数
 * @returns {Promise<void>}
 */
export const connect = async (
  {
    username = "",
    password = "",
    host = "localhost",
    port = 6379,
    db = 0,
    isLog = false,
    errHandler = (err) => console.error("Redis 连接失败：", err),
    connectHandler = () => console.info("Redis 连接成功"),
    readyHandler = () => console.info("Redis 准备就绪"),
    reconnectingHandler = () => console.warn("Redis 正在重连..."),
    endHandler = () => console.warn("Redis 连接已断开")
  }: {
    username?: string;
    password?: string;
    host?: string;
    port?: number;
    db?: number;
    isLog?: boolean;
    errHandler?: (err: Error) => void;
    connectHandler?: () => void;
    readyHandler?: () => void;
    reconnectingHandler?: () => void;
    endHandler?: () => void;
  }
) => {
  // 如果已经连接过，则直接返回
  if (client) return;
  // 拼接 Redis 连接 URL
  let url = "redis://";
  if (!isEmpty(username)) {
    url += username;
    if (!isEmpty(password)) url += `:${password}`;
    url += "@";
  }
  url += host;
  url += `:${port}`;
  url += `/${db}`;
  if (isLog) {
    console.log("Redis 连接 URL: ", url);
    console.log("连接 Redis ...");
  }
  // 创建并保存 Redis 连接对象
  client = await createClient({ url })
    .on("error", errHandler)
    .on("connect", connectHandler)
    .on("ready", readyHandler)
    .on("reconnecting", reconnectingHandler)
    .on("end", endHandler)
    .connect();
};

/**
 * 向 Redis 中设置键值对，如果没有连接过 Redis，需要先调用 {@link connect} 方法进行连接，
 * 否则会直接返回 false
 *
 * @param {string} key 键
 * @param value 值
 * @param {number} expire 过期时间，单位秒，默认值 -1 表示永不过期
 * @param {TimeUnit} unit 过期时间单位，默认值 'second' 表示秒
 * @param {boolean} nx 是否只有键值对不存在时才对键值对进行设置操作
 * @param {boolean} xx 是否只有键值对存在时才对键值对进行设置操作
 * @returns {Promise<boolean>} 设置是否成功，成功返回 true，失败返回 false
 */
export const set = async (
  {
    key,
    value,
    expire = -1,
    unit = TimeUnit.second,
    nx = false,
    xx = false
  }: {
    key: string;
    value: any;
    expire?: number;
    unit?: TimeUnitType;
    nx?: boolean;
    xx?: boolean;
  }
) => {
  // 如果没有连接过 Redis，
  if (!client) return false;
  // 序列化值
  value = JSON.stringify(value);
  // 向 Redis 中设置键值对
  if (expire === 0) return true;
  try {
    if (expire < 0) client.set(key, value, {
      NX: nx,
      XX: xx
    });
    else if (expire > 0) {
      // 过期时间单位转换为秒
      expire = timeConvert(expire, unit, TimeUnit.second);
      client.set(key, value, {
        EX: expire,
        NX: nx,
        XX: xx
      });
    }
    return true;
  } catch (err) {
    console.error("向 Redis 中设置键值对：", err);
    return false;
  }
};

/**
 * 从 Redis 中获取键值对，如果没有连接过 Redis，需要先调用 {@link connect} 方法进行连接，
 * 否则会直接返回 null
 *
 * @param {string} key 键
 * @returns {Promise<string|null>} 键对应的值
 */
export const get = async (key: string): Promise<string | null> => {
  // 如果没有连接过 Redis
  if (!client) return null;
  // 从 Redis 中获取键值对
  return await client.get(key);
};

/**
 * 从 Redis 中获取键值对并将查询结果反序列化转换为指定类型的对象，如果转换失败，则直接返回查询
 * 出来的结果<br/>
 * 如果没有连接过 Redis，需要先调用 {@link connect} 方法进行连接，否则会直接返回 null
 *
 * @template T 查询结果反序列化要转换为的对象类型
 * @param {string} key 键
 * @returns {Promise<T | null>} 键对应的值
 */
export const getWithParse = async <T extends any>(key: string): Promise<T | null> => {
  // 如果没有连接过 Redis
  if (!client) return null;
  // 从 Redis 中获取键值对
  const value = await client.get(key);
  // 反序列化值
  try {
    return JSON.parse(value) as T;
  } catch (e) {
    return value;
  }
};

/**
 * 获取 Redis 中是否存在某个键，如果没有连接过 Redis，需要先调用 {@link connect} 方法进行连接，
 * 否则会直接返回 false
 *
 * @param {string} key 键
 * @returns {Promise<boolean>} 键是否存在，存在返回 true，不存在返回 false
 */
export const has = async (key: string) => {
  // 如果没有连接过 Redis
  if (!client) return false;
  // 获取 Redis 中是否存在某个键
  return !!(await get(key));
};

/**
 * 删除指定的键
 * @param key 要删除的键
 * @returns {Promise<boolean>} 是否删除成功
 */
export const del = async (key: string): Promise<boolean> => {
  if (!client) return false;
  try {
    await client.del(key);
    return true;
  } catch (err) {
    console.error("从 Redis 中删除键值对失败：", err);
    return false;
  }
};

/**
 * 根据模式删除匹配的键
 * @param pattern 匹配模式
 * @returns {Promise<boolean>} 是否删除成功
 */
export const delByPattern = async (pattern: string): Promise<boolean> => {
  if (!client) return false;
  try {
    // 先查找所有匹配的键
    const keys = await client.keys(pattern);
    if (keys.length > 0) {
      // 批量删除
      await client.del(keys);
    }
    return true;
  } catch (err) {
    console.error("从 Redis 中批量删除键值对失败：", err);
    return false;
  }
};